我们在自定义View时,通常使用invalidate方法来刷新View,本篇将对invalidate的实现进行分析。invalidate有多个重载方法,
但其最终的实现都是类似的,这里我们从invalidate()开始分析。
1 | frameworks/base/core/java/android/view/View.java |
invalidate的实现很简单,只需要满足一定的条件,就需要走绘制流程,这里需要注意两个标记一个是PFLAG_INVALIDATED,代表了View当前已经失效了,
另一个PFLAG_DRAWING_CACHE_VALID,表示View的绘制缓存有效。这里根据参数invalidateCache为true对其进行设置,对于硬件加速和非硬件加速的情况分别走
不同的流程,现在大多数其实是走硬件加速的流程,但这里我们还是看软件绘制的流程,关于硬件加速我们在别的篇章中介绍,在这里Mark~一下就好。
这里会设置一个Rect区域,这个区域实际上就是我们View的大小,也是待刷新的dirty区域,因为mRight-mLeft是View的宽度,mBottom-mTop就是View的高度,随后通过调用ViewParent的
invalidateChild方法,这里的ViewParent实际上就是当前view的父View或者view层级数的ViewRoot,所以invalidateChild是会走到ViewGroup或者ViewRootImpl
中去的。我们接着看哈~
1 | public final void invalidateChild(View child, final Rect dirty) { |
invalidateChild接收两个参数,发起invalidate的child view即dirty区域,dirty区域的起始坐标点就是child view
在父view中的左上角坐标,随后进行do…while循环,在循环中我们忽略执行动画的情况,然后为当前父view设置dirty标记。
这个dirty标记为父view记录下子view的视图情况,后续在draw时会用到该标记判断是否需要绘制通过onDraw绘制view
随后调用invalidateChildInParent方法并返回当前父View的父View,一直到ViewRootImpl。可见这个循环其实是一个在View
层级数中从发起View的父View不断上溯到ViewRoot执行invalidateChildInParent的过程,那么在这之间,invalidateChildInParent
到底会做些什么事情呢?这里我们传递给invalidateChildInParent的是dirty区域的在当前父view的左上角的坐标和dirty区域。
1 | // /frameworks/base/core/java/android/view/ViewGroup.java |
在invalidateChildInParent中,首先通过offset计算dirty区域在当前父view中偏移位置,起始的dirty区域就是发起invalidate的子view
它再父view中的左上角位置就是(mLeft,mTop),随后计算dirty区域,首先判断FLAG_CLIP_CHILDREN是否设置,即我们在布局文件中
设置的android:clipChildren,默认是设置为true的,这个属性是用来限制子view是否在父view的绘制区域内的。设置为false即FLAG_CLIP_CHILDREN未设置的情况下
表示可以超出父view的绘制区域。这种情况下的话就设置dirty区域为当前父view的区域。否则FLAG_CLIP_CHILDREN设置就根据当前dirty区域和父view的区域做交集运算后
得到的dirty区域。如果没交集则dirty置空。然后更新location为当前父view在其父view中的左上角位置,为下一次计算脏区域在其父view中的偏移做准备。这样经过一层层的
计算后最终回溯到ViewRootImpl中的invalidateChildInParent中。
1 | // /frameworks/base/core/java/android/view/ViewRootImpl.java |
checkThread会检查更新的线程是否是ui线程,如果dirty区域为null说明需要绘制整个view树,如果dirty区域为空或者未执行动画也不需要
再进行下去了。如果mCurScrollY不为空说明页面有滚动过,需要据此重新计算dirty区域。随后将dirty区域添加到localDrity中即当前view树
中的dirty区域中去。接着讲当前dirty区域和整个页面区域做交集计算,intersected一般为true也就是有交集,最后通过scheduleTraversals
进行重绘的操作。
1 | // /frameworks/base/core/java/android/view/ViewRootImpl.java |
scheduleTraversals通过performDraw来绘制view,这里实际上是调用draw(boolean fullRedrawNeeded, boolean updateTranformHint)方法,
对应非硬件加速的情况,这个方法内部调用drawSoftware来进行绘制,注意这里的dirty实际上就是当前view树计算后得到的脏区域。当然也包括了我们之前
调用view的invalidate后计算的脏区域。通过这个脏区域通过在mSurface中设置一个裁剪区域并返回一个Canvas,随后的绘制就在此Canvas的裁剪区域中进行绘制。
View的draw绘制过程主要包括以下方面:
- Draw the background
- If necessary, save the canvas’ layers to prepare for fading
- Draw view’s content
- Draw children
- If necessary, draw the fading edges and restore layers
- Draw decorations (scrollbars for instance)
1 | public void draw(Canvas canvas) { |
在draw流程中是否回调onDraw是由dirtyOpaque决定的,而dirtyOpaque是根据标记PFLAG_DIRTY_OPAQUE是否设置来决定的,
还记得在上面invalidateChild时会为父view设置这个标记?这个标记表明子view是不透明的且没有在执行动画,那么此时
就没必要对view进行绘制了,因为子view是在父view之上的,会覆盖掉当前view的视图,所有就没有必要绘制了。
需要注意的是,ViewGroup作为一个容器控件,默认情况下是没有任何东西可画的,它是一个透明控件。
draw过程中的dispatchDraw用来绘制子view,我们看下ViewGroup中实现
1 | // /frameworks/base/core/java/android/view/ViewGroup.java |
drawChild会调用view的另一个重载方法,它有三个参数。
1 | // /frameworks/base/core/java/android/view/View.java |
从draw方法中可以看出,子view的并不是每次进行绘制流程时候都需要绘制一遍,尤其是当view通过invalidate触发绘制时,
因此此时Canvas根据dirty区域设置了裁剪区域,而ViewGroup在绘制子view时会判断其区域是否落在这个裁剪区域内,如果
不在就没有必要进行绘制了,直接返回。这是通过canvas的quickReject进行判断的.随后就是字view的绘制,这里面会判断是否
设置了PFLAG_SKIP_DRAW,这个标记用来控制是否需要对View进行绘制,对ViewGroup来说默认是设置了的,所以它直接通过
dispatchDraw来绘制子view,并不会对自身进行绘制,onDraw也不会进行调用。如果想要使它绘制可以通过setWillNotDraw(false)
来清除PFLAG_SKIP_DRAW标记。这样会进入view(ViewGroup)的draw流程,但具体能不能调用onDraw还要做以下判断
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); //子view是否不透明且未执行动画
1 |
|
到这里invalidate的流程就分析完了,需要注意的是,invalidate会触发绘制流程,但是并不会触发onMeasure和onLayout。